/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.wear.watchface

import android.opengl.EGL14
import android.view.SurfaceHolder
import androidx.annotation.IntRange
import androidx.annotation.Px
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.wear.watchface.Renderer.SharedAssets
import androidx.wear.watchface.style.CurrentUserStyleRepository
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import kotlin.coroutines.resume
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex

internal val EGL_CONFIG_ATTRIB_LIST =
    intArrayOf(
        EGL14.EGL_RENDERABLE_TYPE,
        EGL14.EGL_OPENGL_ES2_BIT,
        EGL14.EGL_RED_SIZE,
        8,
        EGL14.EGL_GREEN_SIZE,
        8,
        EGL14.EGL_BLUE_SIZE,
        8,
        EGL14.EGL_ALPHA_SIZE,
        8,
        EGL14.EGL_NONE
    )

internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)

/**
 * [ListenableFuture]-based compatibility wrapper around [Renderer.GlesRenderer]'s suspending
 * methods.
 */
@Deprecated(message = "Use ListenableGlesRenderer2 instead")
@Suppress("Deprecation")
public abstract class ListenableGlesRenderer
@Throws(GlesException::class)
@JvmOverloads
constructor(
    surfaceHolder: SurfaceHolder,
    currentUserStyleRepository: CurrentUserStyleRepository,
    watchState: WatchState,
    @IntRange(from = 0, to = 60000) interactiveDrawModeUpdateDelayMillis: Long,
    eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
    eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
) :
    Renderer.GlesRenderer(
        surfaceHolder,
        currentUserStyleRepository,
        watchState,
        interactiveDrawModeUpdateDelayMillis,
        eglConfigAttribList,
        eglSurfaceAttribList
    ) {
    /**
     * Inside of a [Mutex] this function sets the GL context associated with the
     * [WatchFaceService.getBackgroundThreadHandler]'s looper thread as the current one, executes
     * [runnable] and finally unsets the GL context.
     *
     * Access to the GL context this way is necessary because GL contexts are not shared between
     * renderers and there can be multiple watch face instances existing concurrently (e.g. headless
     * and interactive, potentially from different watch faces if an APK contains more than one
     * [WatchFaceService]).
     *
     * NB this function is called by the library before running [runBackgroundThreadGlCommands] so
     * there's no need to use this directly in client code unless you need to make GL calls outside
     * of those methods.
     *
     * @throws [IllegalStateException] if the calls to [EGL14.eglMakeCurrent] fails
     */
    @WorkerThread
    public fun runBackgroundThreadGlCommands(runnable: Runnable) {
        runBlocking { runBackgroundThreadGlCommands { runnable.run() } }
    }

    /**
     * Inside of a [Mutex] this function sets the UiThread GL context as the current one, executes
     * [runnable] and finally unsets the GL context.
     *
     * Access to the GL context this way is necessary because GL contexts are not shared between
     * renderers and there can be multiple watch face instances existing concurrently (e.g. headless
     * and interactive, potentially from different watch faces if an APK contains more than one
     * [WatchFaceService]).
     *
     * @throws [IllegalStateException] if the calls to [EGL14.eglMakeCurrent] fails
     */
    @UiThread
    public fun runUiThreadGlCommands(runnable: Runnable) {
        runBlocking { runUiThreadGlCommands { runnable.run() } }
    }

    /**
     * Called once a background thread when a new GL context is created on the background thread,
     * before any subsequent calls to [render]. Note this function is called inside a lambda passed
     * to [runBackgroundThreadGlCommands] which has synchronized access to the GL context. Note
     * cancellation of the returned future is not supported.
     *
     * @return A ListenableFuture<Unit> which is resolved when background thread work has completed.
     *   Rendering will be blocked until this has resolved.
     */
    @Suppress("AsyncSuffixFuture") // This is the guava wrapper for a suspend function
    protected open fun onBackgroundThreadGlContextCreatedFuture(): ListenableFuture<Unit> {
        return SettableFuture.create<Unit>().apply { set(Unit) }
    }

    override suspend fun onBackgroundThreadGlContextCreated(): Unit = suspendCancellableCoroutine {
        val future = onBackgroundThreadGlContextCreatedFuture()
        future.addListener({ it.resume(future.get()) }, { runnable -> runnable.run() })
    }

    /**
     * Called when a new GL surface is created on the UiThread, before any subsequent calls to
     * [render] and in response to [SurfaceHolder.Callback.surfaceChanged]. Note this function is
     * called inside a lambda passed to [Renderer.GlesRenderer.runUiThreadGlCommands] which has
     * synchronized access to the GL context. Note cancellation of the returned future is not
     * supported.
     *
     * @param width width of surface in pixels
     * @param height height of surface in pixels
     * @return A ListenableFuture<Unit> which is resolved when UI thread work has completed.
     *   Rendering will be blocked until this has resolved.
     */
    @UiThread
    @Suppress("AsyncSuffixFuture") // This is the guava wrapper for a suspend function
    protected open fun onUiThreadGlSurfaceCreatedFuture(
        @Px width: Int,
        @Px height: Int
    ): ListenableFuture<Unit> {
        return SettableFuture.create<Unit>().apply { set(Unit) }
    }

    override suspend fun onUiThreadGlSurfaceCreated(@Px width: Int, @Px height: Int): Unit =
        suspendCancellableCoroutine {
            val future = onUiThreadGlSurfaceCreatedFuture(width, height)
            future.addListener({ it.resume(future.get()) }, { runnable -> runnable.run() })
        }
}

/**
 * [ListenableFuture]-based compatibility wrapper around [Renderer.GlesRenderer]'s suspending
 * methods.
 */
public abstract class ListenableGlesRenderer2<SharedAssetsT>
@Throws(GlesException::class)
@JvmOverloads
constructor(
    surfaceHolder: SurfaceHolder,
    currentUserStyleRepository: CurrentUserStyleRepository,
    watchState: WatchState,
    @IntRange(from = 0, to = 60000) interactiveDrawModeUpdateDelayMillis: Long,
    eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
    eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
) :
    Renderer.GlesRenderer2<SharedAssetsT>(
        surfaceHolder,
        currentUserStyleRepository,
        watchState,
        interactiveDrawModeUpdateDelayMillis,
        eglConfigAttribList,
        eglSurfaceAttribList
    ) where SharedAssetsT : SharedAssets {
    /**
     * Inside of a [Mutex] this function sets the GL context associated with the
     * [WatchFaceService.getBackgroundThreadHandler]'s looper thread as the current one, executes
     * [runnable] and finally unsets the GL context.
     *
     * Access to the GL context this way is necessary because GL contexts are not shared between
     * renderers and there can be multiple watch face instances existing concurrently (e.g. headless
     * and interactive, potentially from different watch faces if an APK contains more than one
     * [WatchFaceService]).
     *
     * NB this function is called by the library before running [runBackgroundThreadGlCommands] so
     * there's no need to use this directly in client code unless you need to make GL calls outside
     * of those methods.
     *
     * @throws [IllegalStateException] if the calls to [EGL14.eglMakeCurrent] fails
     */
    @WorkerThread
    public fun runBackgroundThreadGlCommands(runnable: Runnable) {
        runBlocking { runBackgroundThreadGlCommands { runnable.run() } }
    }

    /**
     * Inside of a [Mutex] this function sets the UiThread GL context as the current one, executes
     * [runnable] and finally unsets the GL context.
     *
     * Access to the GL context this way is necessary because GL contexts are not shared between
     * renderers and there can be multiple watch face instances existing concurrently (e.g. headless
     * and interactive, potentially from different watch faces if an APK contains more than one
     * [WatchFaceService]).
     *
     * @throws [IllegalStateException] if the calls to [EGL14.eglMakeCurrent] fails
     */
    @UiThread
    public fun runUiThreadGlCommands(runnable: Runnable) {
        runBlocking { runUiThreadGlCommands { runnable.run() } }
    }

    /**
     * Called once a background thread when a new GL context is created on the background thread,
     * before any subsequent calls to [render]. Note this function is called inside a lambda passed
     * to [runBackgroundThreadGlCommands] which has synchronized access to the GL context. Note
     * cancellation of the returned future is not supported.
     *
     * @return A ListenableFuture<Unit> which is resolved when background thread work has completed.
     *   Rendering will be blocked until this has resolved.
     */
    @Suppress("AsyncSuffixFuture") // This is the guava wrapper for a suspend function
    protected open fun onBackgroundThreadGlContextCreatedFuture(): ListenableFuture<Unit> {
        return SettableFuture.create<Unit>().apply { set(Unit) }
    }

    final override suspend fun onBackgroundThreadGlContextCreated(): Unit =
        suspendCancellableCoroutine {
            val future = onBackgroundThreadGlContextCreatedFuture()
            future.addListener({ it.resume(future.get()) }, { runnable -> runnable.run() })
        }

    /**
     * Called when a new GL surface is created on the UiThread, before any subsequent calls to
     * [render] and in response to [SurfaceHolder.Callback.surfaceChanged]. Note this function is
     * called inside a lambda passed to [Renderer.GlesRenderer.runUiThreadGlCommands] which has
     * synchronized access to the GL context. Note cancellation of the returned future is not
     * supported.
     *
     * @param width width of surface in pixels
     * @param height height of surface in pixels
     * @return A ListenableFuture<Unit> which is resolved when UI thread work has completed.
     *   Rendering will be blocked until this has resolved.
     */
    @UiThread
    @Suppress("AsyncSuffixFuture") // This is the guava wrapper for a suspend function
    protected open fun onUiThreadGlSurfaceCreatedFuture(
        @Px width: Int,
        @Px height: Int
    ): ListenableFuture<Unit> {
        return SettableFuture.create<Unit>().apply { set(Unit) }
    }

    final override suspend fun onUiThreadGlSurfaceCreated(@Px width: Int, @Px height: Int): Unit =
        suspendCancellableCoroutine {
            val future = onUiThreadGlSurfaceCreatedFuture(width, height)
            future.addListener({ it.resume(future.get()) }, { runnable -> runnable.run() })
        }

    /**
     * When editing multiple [WatchFaceService] instances and hence Renderers can exist concurrently
     * (e.g. a headless instance and an interactive instance) and using [SharedAssets] allows memory
     * to be saved by sharing immutable data (e.g. Bitmaps, shaders, etc...) between them.
     *
     * To take advantage of SharedAssets, override this method. The constructed SharedAssets are
     * passed into the [render] as an argument (NB you'll have to cast this to your type). It is
     * safe to make GLES calls within this method.
     *
     * When all instances using SharedAssets have been closed, [SharedAssets.onDestroy] will be
     * called.
     *
     * Note that while SharedAssets are constructed on a background thread, they'll typically be
     * used on the main thread and subsequently destroyed there. The watch face library constructs
     * shared GLES contexts to allow resource sharing between threads.
     *
     * @return A [ListenableFuture] for the [SharedAssetsT] that will be passed into [render] and
     *   [renderHighlightLayer]
     */
    @WorkerThread
    @Suppress("AsyncSuffixFuture") // This is the guava wrapper for a suspend function
    public abstract fun createSharedAssetsFuture(): ListenableFuture<SharedAssetsT>

    final override suspend fun createSharedAssets(): SharedAssetsT = suspendCancellableCoroutine {
        val future = createSharedAssetsFuture()
        future.addListener({ it.resume(future.get()) }, { runnable -> runnable.run() })
    }
}
